/**
 * Nest.js - nested structures construction library.
 * http://code.google.com/p/nest-js/
 * Copyright (c) 2011 Ruslan Matveev
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * Entering local execution context.
 * @param {Object} window Global object to attach namespace.
 * @param {*=} undefined Reference to undefined.
 */
(function(window, undefined) {

	'use strict';

	/**
	 * Executes a provided function once per array element.
	 * @param {Function} callback Function to execute for each element.
	 * @param {Object} thisObject Value to use as this when executing callback.
	 */
	if (!('forEach' in Array.prototype)) {
		Array.prototype.forEach = function(callback, thisObject) {
			for (var i = 0, n = this.length; i < n; i++) {
				if (i in this) {
					callback.call(thisObject, this[i], i, this);
				}
			}
		};
	}

	/**
	 * Returns the first index at which a given element can be found
	 * in the array, or -1 if it is not present.
	 * @param {*} searchElement Element to locate in the array.
	 * @param {number} fromIndex The index at which to begin the search.
	 * @return {number} The index of the first occurrence of an element.
	 */
	if (!('indexOf' in Array.prototype)) {
		Array.prototype.indexOf = function(searchElement, fromIndex) {
			if (fromIndex === undefined) fromIndex = 0;
			if (fromIndex < 0) fromIndex += this.length;
			if (fromIndex < 0) fromIndex = 0;
			for (var n = this.length; fromIndex < n; fromIndex++) {
				if (fromIndex in this && this[fromIndex] === searchElement) {
					return fromIndex;
				}
			}
			return -1;
		};
	}

	/**
	 * Creates a new function that, when called, itself calls this function
	 * in the context of the provided this value, with a given sequence of
	 * arguments preceding any provided when the new function was called.
	 * @param {*} owner The value to be passed as the this parameter
	 *     to the target function when the bound function is called.
	 */
	if (!('bind' in Function.prototype)) {
		Function.prototype.bind = function(owner) {
			var that = this;
			if (arguments.length <= 1) {
				return function() {
					return that.apply(owner, arguments);
				};
			} else {
				var args = args2arr(arguments, 1);
				return function() {
					return that.apply(owner,
						arguments.length === 0 ?
						args :
						args.concat(args2arr(arguments))
					);
				};
			}
		};
	}

	/**
	 * List of the base types.
	 * @type {Array.<string>}
	 * @const
	 */
	var BASE_TYPES = [
		'undefined', 'null',
		'boolean', 'number', 'string',
		'function', 'array', 'object'
	];

	/**
	 * List of the reserved words, cannnot be used as a function names.
	 * @type {Array.<string>}
	 * @const
	 */
	var RESERVED_WORDS = [
		'break', 'else', 'new', 'var', 'case', 'finally',
		'return', 'void', 'catch', 'for', 'switch', 'while',
		'continue', 'function', 'this', 'with', 'default',
		'if', 'throw', 'delete', 'in', 'try', 'do', 'instanceof',
		'typeof', 'abstract', 'enum', 'int', 'short', 'boolean',
		'export', 'interface', 'static', 'byte', 'extends',
		'long', 'super', 'char', 'final', 'native', 'synchronized',
		'class', 'float', 'package', 'throws', 'const', 'goto',
		'private', 'transient', 'debugger', 'implements', 'protected',
		'volatile', 'double', 'import', 'public', 'null', 'true', 'false'
	];

	/**
	 * Returns true if the value is undefined.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is undefined,
	 *     otherwise false.
	 */
	function isUndefined(value) {
		return (typeof(value) === 'undefined');
	}

	/**
	 * Returns true if the value is null.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is null, otherwise false.
	 */
	function isNull(value) {
		return (value === null);
	}

	/**
	 * Returns true if the value is string.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is string, otherwise false.
	 */
	function isString(value) {
		return (typeof(value) === 'string');
	}

	/**
	 * Returns true if the value is number.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is number, otherwise false.
	 */
	function isNumber(value) {
		return (typeof(value) === 'number');
	}

	/**
	 * Returns true if the value is boolean.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is boolean, otherwise false.
	 */
	function isBoolean(value) {
		return (typeof(value) === 'boolean');
	}

	/**
	 * Returns true if the value is function.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is function, otherwise false.
	 */
	function isFunction(value) {
		return (typeof(value) === 'function');
	}

	/**
	 * Returns true if the value is object.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is object, otherwise false.
	 */
	function isObject(value) {
		// return true only if the object is not null
		return (!isNull(value) && typeof(value) === 'object');
	}

	/**
	 * Returns true if the value is array.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is array, otherwise false.
	 */
	function isArray(value) {
		// return true if value is object and array
		return (isObject(value) && value.constructor === Array);
	}

	/**
	 * Returns true if the value type belongs to the one of the base types.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value type belongs
	 *     to the one of the base types.
	 */
	function isBaseTypeName(value) {
		// check if the value is in the base types array
		return (isString(value) && BASE_TYPES.indexOf(value) !== -1);
	}

	/**
	 * Returns value's base type (according to the base types list).
	 * @param {*} value Value to check.
	 * @return {string|*} Returns value's base type name.
	 */
	function getBaseType(value) {
		// get type of the value
		var valueType = typeof(value);
		// check exceptions
		if (isNull(value)) {
			// value is null
			return 'null';
		} else if (isArray(value)) {
			// value is array
			return 'array';
		} else if (isBaseTypeName(valueType)) {
			// return valueType
			return valueType;
		}
	}

	/**
	 * Returns true if the value is valid JavaScript identifier.
	 * @param {*} value Value to check.
	 * @return {boolean} Returns true if the value is valid
	 *     JavaScript identifier, otherwise false.
	 */
	function isIdentifier(value) {
		// check if the value is reserved word
		if (isString(value) && RESERVED_WORDS.indexOf(value) === -1) {
			// skip 'undefined' though it's valid identifier
			if (value !== 'undefined') {
				// check value against identifier regexp pattern
				return (/^[a-zA-Z_$]+[0-9a-zA-Z_$]*$/).test(value);
			}
		}
		// value is not a string or it's reseved word, return false
		return false;
	}

	/**
	 * Retrieves the list of function arguments.
	 * @param {Function} functionObject Function object to process.
	 * @return {Array.<string>} Array of function arguments names.
	 */
	function getFunctionArguments(functionObject) {
		// check arguments
		if (!isFunction(functionObject)) return [];
		// how many arguments do we have
		var functionArity = functionObject.length;
		// convert functionObject to string
		var functionValue = functionObject.toString();
		// extract function arguments
		var functionArguments = functionValue.split(')', 2).shift();
		functionArguments = functionArguments.split('(', 2).pop();
		// return arguments names array
		return functionArguments.split(/\s?,\s?/, functionArity);
	}

	/**
	 * Extracts function body from the function object
	 * and returns it as a string.
	 * @param {Function} functionObject Function object to process.
	 * @return {string} Function body as a string.
	 */
	function getFunctionBody(functionObject) {
		// check arguments
		if (!isFunction(functionObject)) return '';
		// convert functionObject to string
		var functionBody = functionObject.toString();
		// extract function body
		functionBody = functionBody.split('{').slice(1).join('{');
		functionBody = functionBody.split('}').slice(0, -1).join('}');
		// remove leading and trailing whitespaces
		functionBody = functionBody.replace(/^\s+/, '').replace(/\s+$/, '');
		// remove trailing semicolon
		functionBody = functionBody.replace(/;$/, '');
		// wrap function body into new line literals
		functionBody = ('\r\n' + functionBody + '\r\n');
		// return extracted body
		return functionBody;
	}

	/**
	 * Sets function arguments names.
	 * @param {Function} functionObject Function object to process.
	 * @param {Array.<string>} argumentsList Array with argument names.
	 * @return {Function} New function object with updated arguments list.
	 */
	function setFunctionArguments(functionObject, argumentsList) {
		// check arguments
		if (!isFunction(functionObject)) functionObject = new Function();
		// set argumentsList to empty array in case if it's not set
		if (isUndefined(argumentsList)) argumentsList = [];
		// wrap argumentsList into array in case if it's not an array
		if (!isArray(argumentsList)) argumentsList = [argumentsList];
		// extract function body
		var functionBody = getFunctionBody(functionObject);
		// create and return new function with updated arguments list
		return new Function(argumentsList, functionBody);
	}

	/**
	 * Calls a function with a given this and arguments provided as a map,
	 * consists of name and value that will be used as an argument name and
	 * argument value respectively.
	 * @param {Function} functionObject Function to call.
	 * @param {Object.<string, *>} argumentsMap Map consists of argument
	 *     name as a string and argument value, if name is numeric then
	 *     it will be used to map original argument's name.
	 * @param {Object} thisObject Object that will be used as this, while
	 *     calling functionObject.
	 * @return {*} Result of functionObject call as it's returned.
	 */
	function applyFunctionArguments(functionObject, argumentsMap, thisObject) {
		// initialize variables
		var argumentNames = [];
		var argumentValues = [];
		// get list of original arguments
		var argumentsList = getFunctionArguments(functionObject);
		// walk over argumentsMap to build arguments
		forEach(argumentsMap, function(value, name) {
			// check if we can convert name to number
			var argumentIndex = parseInt(name, 10);
			// check if we can use original argument name
			if (!isNaN(argumentIndex) && argumentsList[argumentIndex]) {
				// add argument name
				argumentNames.push(argumentsList[argumentIndex]);
				// add argument value
				argumentValues.push(value);
			} else if (isIdentifier(name)) {
				// add argument name
				argumentNames.push(name);
				// add argument value
				argumentValues.push(value);
			}
		});
		// create evaluator function out of functionObject with new arguments
		var evaluator = setFunctionArguments(functionObject, argumentNames);
		// execute evaluator with new arguments and thisObject as this
		return evaluator.apply(thisObject, argumentValues);
	}

	/**
	 * Converts arguments object into array of arguments.
	 * @param {Object} args Arguments object to process.
	 * @param {number=} index First argument offset.
	 * @return {Array.<*>} Arguments array.
	 */
	function args2arr(args, index) {
		// call Array.prototype.slice on arguments object
		return Array.prototype.slice.call(args, index || 0);
	}

	/**
	 * Executes a provided callback once per array element or object property.
	 * @param {Object} object Array or object to process.
	 * @param {Function} callback Function to execute for each
	 *     element or property.
	 * @param {*} thisObject this object that will be used as this
	 *     while calling the callback function
	 * @return {*} Original thisObject.
	 */
	function forEach(object, callback, thisObject) {
		// check if the given object is an array
		if (isArray(object)) {
			// use built-in Array.prototype.forEach
			object.forEach.call(
				// array and callback
				object, callback,
				// thisObject or this if not specified
				thisObject || this
			);
		} else {
			// check if we have an object or function
			if (isObject(object) || isFunction(object)) {
				// walk over the properties
				for (var key in object) {
					// enumerate only own properties
					if (!object.hasOwnProperty(key)) continue;
					// execute callback for each object property
					callback.call(
						// thisObject or this if not specified
						thisObject || this,
						// simulate Array.prototype.forEach convention
						object[key], key, object
					);
				}
			}
		}
		// return thisObject as a result
		return thisObject;
	}

	/**
	 * Prepares contextObject's properties for nested calls.
	 * @param {Object.<string, *>} contextObject Map consists of
	 *     property name as a string and property value.
	 * @param {Object} thisObject Object that will be used as parent, while
	 *     calling function properties of the contextObject.
	 * @return {Object.<string, *>} Original thisObject,
	 *     where each property is binded to it's parent.
	 */
	function ASTPrepareProperties(contextObject, thisObject) {
		var parentElement = (arguments[2] || thisObject);
		return forEach(contextObject, function(value, key) {
			if (isFunction(value)) thisObject[key] = function() {
				var result = value.apply(this, arguments);
				parentElement.value.push({
					'type': result.type,
					'value': result.value
				});
				return parentElement;
			}; else thisObject[key] = value;
			ASTPrepareProperties(value, thisObject[key], parentElement);
		}, thisObject);
	}

	/**
	 * Generates abstract syntax tree out of it's own calls.
	 * @param {string} elementName Name of the element.
	 * @return {Function} Object that represents abstract syntax tree.
	 */
	function ASTGenerator(elementName) {
		// prepare next call wrapper
		var appendElement = (function() {
			// call parent function with updated arguments list
			var result = ASTGenerator.apply(this, args2arr(arguments));
			// merge own arguments list with subcall arguments list
			result.value = appendElement.value.concat(result.value);
			return result;
		}).bind(this, elementName);
		// save element name
		appendElement.type = elementName;
		// save element value
		appendElement.value = [args2arr(arguments, 1)];
		// return nested call wrapper function
		return appendElement;
	}

	/**
	 * Enriches abstract syntax tree generated by ASTGenerator with the
	 * type information and converts into object that is easy to work with.
	 * @param {*} object Object to process.
	 * @return {*} Object that represents abstract syntax tree.
	 */
	function createAST(object) {
		// initialize variables
		var objectValue, objectType;
		// check if we have element's function call
		if ((isFunction(object) || isObject(object)) && object.type) {
			// initialize value
			objectValue = [];
			// set type to element name
			objectType = object.type;
			// build nested tree
			forEach(object.value, function(list, index) {
				// process children
				objectValue.push(createAST(list).value);
			});
			if (isObject(object)) objectValue = {
				'type': objectType, 'value': objectValue.pop()
			};
		} else {
			// initialize simple type value
			objectValue = object;
			// initialize object's base type
			objectType = getBaseType(object);
			// check if we have a collection here
			if (isObject(object)) {
				// initialize value
				objectValue = (isArray(object) ? [] : {});
				// build nested tree
				forEach(object, function(value, index) {
					// process children
					objectValue[index] = createAST(value);
				});
			}
		}
		// return object that represents AST leaf
		return {
			// element type or base type
			'type': objectType,
			// element's children or value
			'value': objectValue
		};
	}

	function define(functionObject) {
		// extract evaluator's body
		var evaluator = getFunctionBody(functionObject);
		// evaluator's body becomes an arguments list
		evaluator = 'return this(' + evaluator + ')';
		// create evaluator function
		evaluator = new Function(evaluator);

		return applyFunctionArguments(evaluator, {
			'element': ASTGenerator.bind(this, 'element')
		}, function() {
			var argumentsTree = args2arr(arguments);
			argumentsTree = createAST(argumentsTree).value;
			return argumentsTree;
		});
	}

	window['njs'] = {
		'isNull': isNull,
		'isUndefined': isUndefined,
		'isString': isString,
		'isNumber': isNumber,
		'isBoolean': isBoolean,
		'isFunction': isFunction,
		'isObject': isObject,
		'isArray': isArray,
		'isBaseTypeName': isBaseTypeName,
		'getBaseType': getBaseType,
		'isIdentifier': isIdentifier,
		'define': define
	};

})(function() { return this; }.call(null));